﻿//------------------------------------------------------------------------------
// The contents of this file are subject to the Microsoft Public License (Ms-PL)
// You may obtain a copy of the License at http://psadmin.codeplex.com/license. 
// 
// Software distributed under the License is distributed on an "AS IS" basis, 
// WITHOUT WARRANTY OF ANY KIND, either express or implied. 
// See the License for the specific language governing rights and limitations 
// under the License.
// 
// The initial developer is Ben Foster (ben.foster@retroviz.com)
// Copyright (c) Retroviz Limited (http://www.retroviz.com).
// All Rights Reserved.
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Management.Automation.Runspaces;
using System.Management.Automation;
using System.Collections.ObjectModel;
using PSAdmin.Core.Interfaces;
using PSAdmin.Core.Domain;
using System.Security.Principal;
using System.Runtime.InteropServices;
using PSAdmin.Core.Utils;

namespace PSAdmin.Services
{
    public class PSScriptService : IScriptService
    {
        private ITemplateService _templateService;
        private ILoggerService _loggerService;

        public PSScriptService(ITemplateService templateService, ILoggerService loggerService) {
            _templateService = templateService;
            _loggerService = loggerService;
        }

        public string ExecuteScript(PSScript script, IDictionary<string, object> data) {
            var commandText = _templateService.ProcessTemplate(script.CommandText, data);
            
            StringBuilder sb = new StringBuilder();
            RunspaceConfiguration config = RunspaceConfiguration.Create();
            
            // TODO - add snapins etc.
            // Load Exchange PowerShell snap-in.
            //config.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);
            //if (warning != null) throw warning;

            _loggerService.Info(string.Format("Beginning impersonation for script {0}", script.Name));
            
            if (script.Impersonate && !(string.IsNullOrEmpty(script.Username) && 
                                        string.IsNullOrEmpty(script.Password) && 
                                        string.IsNullOrEmpty(script.Domain))) {
                var password = EncryptionHelper.Decrypt(script.Password);
                BeginImpersonation(script.Username, password, script.Domain);
            }
            
            Runspace runspace = RunspaceFactory.CreateRunspace(config);

            if (runspace.RunspaceStateInfo.State == RunspaceState.BeforeOpen) {
                runspace.Open();
            }

            using (Pipeline pipeline = runspace.CreatePipeline())
            {
                pipeline.Commands.AddScript(commandText);                   

                // format results as text
                var outStringCommand = new Command("Out-String");
                outStringCommand.Parameters.Add("Width", 900);

                Collection<PSObject> results;

                try {
                    results = pipeline.Invoke();
                }
                catch (CommandNotFoundException cnfex) {
                    return string.Format("The command {0} does not exist.", cnfex.CommandName);
                }
                catch (Exception ex) {
                    return ex.Message;
                }
                finally {
                    EndImpersonation();
                }

                foreach (var item in results) {
                    sb.AppendLine(item.ToString());
                }
            }

            runspace.Close();
            
            return string.Format("\n{0}: PSAdmin -> {1}\n{2}\n\n", 
                DateTime.Now.ToLongTimeString(), script.Name, sb.ToString());
        }

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

        private IntPtr _userToken = IntPtr.Zero;
        private WindowsImpersonationContext _impersonationContext = null;

        private void BeginImpersonation(string username, string password, string domain)
        {
            EndImpersonation();

            _loggerService.Info("User Before Impersonation: " + WindowsIdentity.GetCurrent().Name);

            bool success = LogonUser(username, domain, password, 2, 0, ref _userToken);

            // did it work?
            if (!success) throw new Exception(string.Format("LogonUser returned error {0}", Marshal.GetLastWin32Error()));

            WindowsIdentity fakeId = new WindowsIdentity(_userToken);
            _impersonationContext = fakeId.Impersonate();
            
            _loggerService.Info("User After Impersonation: " + WindowsIdentity.GetCurrent().Name);
        }

        private void EndImpersonation()
        {
            if (_impersonationContext != null)
            {
                try
                {
                    _impersonationContext.Undo();
                }

                finally
                {
                    _impersonationContext.Dispose();
                    _impersonationContext = null;
                }
            }
        }
    }
}
